基于hyperledger联盟链的汽车轨迹追溯系统(一)
2021/5/3 20:29:03
本文主要是介绍基于hyperledger联盟链的汽车轨迹追溯系统(一),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
基于hyperledger联盟链的汽车轨迹追溯系统
- 一、koa架构整理
- 二、链码整理
一、koa架构整理
在完成之前的六个流程之后,已经完成一个简易的可交互的前后端系统,在此基础上进行系统开发
之前的后端架构可移植性不好,这里进行后端架构的整理
在controller文件夹下建立工具模块tool.js
用于封装所有与区块链相关的业务(交易,查询,根据hash回溯交易)
const Fabric_Client = require('fabric-client'); const path = require('path'); const os = require('os') const util = require('util') const fs = require('fs'); // const theStotePath = '/home/zatnr/文档/sitpProject/20210302/carChain/app/hfc-key-store'; const theStotePath = './hfc-key-store'; async function excuteQuery (sendRequest) { const title = 'admin page' let result = '' var fabric_client = new Fabric_Client(); var key = "name" // setup the fabric network var channel = fabric_client.newChannel('mychannel'); var peer = fabric_client.newPeer('grpc://localhost:7051'); channel.addPeer(peer); // var member_user = null; // var store_path = path.join(os.homedir(), '.hfc-key-store'); var store_path = path.join(theStotePath); console.log('Store path:'+store_path); // create the key value store as defined in the fabric-client/config/default.json 'key-value-store' setting result = await Fabric_Client.newDefaultKeyValueStore({ path: store_path }).then((state_store) => { // assign the store to the fabric client fabric_client.setStateStore(state_store); var crypto_suite = Fabric_Client.newCryptoSuite(); // use the same location for the state store (where the users' certificate are kept) // and the crypto store (where the users' keys are kept) var crypto_store = Fabric_Client.newCryptoKeyStore({path: store_path}); crypto_suite.setCryptoKeyStore(crypto_store); fabric_client.setCryptoSuite(crypto_suite); // get the enrolled user from persistence, this user will sign all requests return fabric_client.getUserContext('user1', true); }).then((user_from_store) => { if (user_from_store && user_from_store.isEnrolled()) { console.log('Successfully loaded user1 from persistence'); member_user = user_from_store; } else { throw new Error('Failed to get user1.... run registerUser.js'); } // queryTuna - requires 1 argument, ex: args: ['4'], const request = sendRequest; // send the query proposal to the peer return channel.queryByChaincode(request); }).then((query_responses) => { console.log("Query has completed, checking results"); // query_responses could have more than one results if there multiple peers were used as targets if (query_responses && query_responses.length == 1) { if (query_responses[0] instanceof Error) { console.error("error from query = ", query_responses[0]); result = "Could not locate tuna" } else { console.log("Response is ", query_responses[0].toString()); return query_responses[0].toString() } } else { console.log("No payloads were returned from query"); result = "Could not locate tuna" } }).catch((err) => { console.error('Failed to query successfully :: ' + err); result = 'Failed to query successfully :: ' + err }); // await ctx.render('index', { // title, result // }) return result } async function executeTranSaction(sendRequest){ console.log(`request.....${sendRequest}`) var fabric_client = new Fabric_Client(); // setup the fabric network var channel = fabric_client.newChannel('mychannel'); var peer = fabric_client.newPeer('grpc://localhost:7051'); var order = fabric_client.newOrderer('grpc://localhost:7050') channel.addOrderer(order); channel.addPeer(peer); var member_user = null; // var store_path = path.join(os.homedir(), '.hfc-key-store'); var store_path = path.join(theStotePath); console.log('Store path:'+store_path); // create the key value store as defined in the fabric-client/config/default.json 'key-value-store' setting let result = await Fabric_Client.newDefaultKeyValueStore({ path: store_path }).then((state_store) => { // assign the store to the fabric client fabric_client.setStateStore(state_store); var crypto_suite = Fabric_Client.newCryptoSuite(); // use the same location for the state store (where the users' certificate are kept) // and the crypto store (where the users' keys are kept) var crypto_store = Fabric_Client.newCryptoKeyStore({path: store_path}); crypto_suite.setCryptoKeyStore(crypto_store); fabric_client.setCryptoSuite(crypto_suite); // get the enrolled user from persistence, this user will sign all requests return fabric_client.getUserContext('user1', true); }).then((user_from_store) => { if (user_from_store && user_from_store.isEnrolled()) { console.log('Successfully loaded user1 from persistence'); member_user = user_from_store; } else { throw new Error('Failed to get user1.... run registerUser.js'); } // get a transaction id object based on the current user assigned to fabric client tx_id = fabric_client.newTransactionID(); console.log("Assigning transaction_id: ", tx_id._transaction_id); // recordTuna - requires 5 args, ID, vessel, location, timestamp,holder - ex: args: ['10', 'Hound', '-12.021, 28.012', '1504054225', 'Hansel'], // send proposal to endorser sendRequest.txId = tx_id; console.log("sendRequest...."+sendRequest.txId) const request = sendRequest; // send the transaction proposal to the peers return channel.sendTransactionProposal(request); }).then((results) => { var proposalResponses = results[0]; var proposal = results[1]; let isProposalGood = false; if (proposalResponses && proposalResponses[0].response && proposalResponses[0].response.status === 200) { isProposalGood = true; console.log('Transaction proposal was good'); } else { console.error('Transaction proposal was bad'); } if (isProposalGood) { console.log(util.format( 'Successfully sent Proposal and received ProposalResponse: Status - %s, message - "%s"', proposalResponses[0].response.status, proposalResponses[0].response.message)); // build up the request for the orderer to have the transaction committed var request = { proposalResponses: proposalResponses, proposal: proposal }; // set the transaction listener and set a timeout of 30 sec // if the transaction did not get committed within the timeout period, // report a TIMEOUT status var transaction_id_string = tx_id.getTransactionID(); //Get the transaction ID string to be used by the event processing var promises = []; var sendPromise = channel.sendTransaction(request); promises.push(sendPromise); //we want the send transaction first, so that we know where to check status // get an eventhub once the fabric client has a user assigned. The user // is required bacause the event registration must be signed //------------------------------------------------------------ // let event_hub = fabric_client.newEventHub('grpc://localhost:7053'); // // let event_hub = new ChannelEventHub(channel, peer); // //接下来设置EventHub,用于监听Transaction是否成功写入,这里也是启用了TLS // let data = fs.readFileSync('/home/fabric/Documents/carChain/basic-network/crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt'); // let grpcOpts = { // 'pem': Buffer.from(data).toString(), // 'ssl-target-name-override': "peer0.org1.example.com" // } // event_hub.setPeerAddr('grpc://localhost:7053',grpcOpts); // event_hub.connect(); // console.log('data:'+data); // console.log('grpcOpts.pem:'+grpcOpts.pem); //------------------------------------------------------------ let event_hub = channel.newChannelEventHub('localhost:7051'); //------------------------------------------------------------ // using resolve the promise so that result status may be processed // under the then clause rather than having the catch clause process // the status let txPromise = new Promise((resolve, reject) => { let handle = setTimeout(() => { event_hub.disconnect(); resolve({event_status : 'TIMEOUT'}); //we could use reject(new Error('Trnasaction did not complete within 30 seconds')); }, 3000); event_hub.connect(); event_hub.registerTxEvent(transaction_id_string, (tx, code) => { // this is the callback for transaction event status // first some clean up of event listener clearTimeout(handle); event_hub.unregisterTxEvent(transaction_id_string); event_hub.disconnect(); // now let the application know what happened var return_status = {event_status : code, tx_id : transaction_id_string}; if (code !== 'VALID') { console.error('The transaction was invalid, code = ' + code); resolve(return_status); // we could use reject(new Error('Problem with the tranaction, event status ::'+code)); } else { //------------------------------------------------------------------------------------------- //console.log('The transaction has been committed on peer ' + event_hub._ep._endpoint.addr); //------------------------------------------------------------------------------------------- console.log('The transaction has been committed on peer ' + event_hub.getPeerAddr()); //------------------------------------------------------------------------------------------- resolve(return_status); } }, (err) => { //this is the callback if something goes wrong with the event registration or processing reject(new Error('There was a problem with the eventhub ::'+err)); }); }); promises.push(txPromise); return Promise.all(promises); } else { console.error('Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...'); throw new Error('Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...'); } }).then((results) => { console.log('Send transaction promise and event listener promise have completed'); // check the results in the order the promises were added to the promise all list if (results && results[0] && results[0].status === 'SUCCESS') { console.log('Successfully sent transaction to the orderer.'); //res.send(tx_id.getTransactionID()); return tx_id.getTransactionID(); } else { console.error('Failed to order the transaction. Error code: ' + response.status); } if(results && results[1] && results[1].event_status === 'VALID') { console.log('Successfully committed the change to the ledger by the peer'); //res.send(tx_id.getTransactionID()); return tx_id.getTransactionID(); } else { console.log('Transaction failed to be committed to the ledger due to ::'+results[1].event_status); } }).catch((err) => { console.error('Failed to invoke successfully :: ' + err); }); return result } async function excuteRecall ( hash ) { var fabric_client = new Fabric_Client(); var key = "name" // setup the fabric network var channel = fabric_client.newChannel('mychannel'); var peer = fabric_client.newPeer('grpc://localhost:7051'); channel.addPeer(peer); // var member_user = null; // var store_path = path.join(os.homedir(), '.hfc-key-store'); var store_path = path.join(theStotePath); console.log('Store path:'+store_path); // create the key value store as defined in the fabric-client/config/default.json 'key-value-store' setting result = await Fabric_Client.newDefaultKeyValueStore({ path: store_path }).then((state_store) => { // assign the store to the fabric client fabric_client.setStateStore(state_store); var crypto_suite = Fabric_Client.newCryptoSuite(); // use the same location for the state store (where the users' certificate are kept) // and the crypto store (where the users' keys are kept) var crypto_store = Fabric_Client.newCryptoKeyStore({path: store_path}); crypto_suite.setCryptoKeyStore(crypto_store); fabric_client.setCryptoSuite(crypto_suite); // get the enrolled user from persistence, this user will sign all requests return fabric_client.getUserContext('user1', true); }).then((user_from_store) => { if (user_from_store && user_from_store.isEnrolled()) { console.log('Successfully loaded user1 from persistence'); member_user = user_from_store; } else { throw new Error('Failed to get user1.... run registerUser.js'); } // send the query proposal to the peer return channel.queryBlockByTxID(hash,peer,true,false); // return channel.queryByChaincode(request); }).then((query_responses) => { console.log("Query has completed, checking results"); // query_responses could have more than one results if there multiple peers were used as targets if (query_responses) { if (query_responses[0] instanceof Error) { console.error("error from query = ", query_responses[0]); result = "Could not locate tuna" } else { console.log("Response is ", query_responses); return query_responses } } else { console.log("No payloads were returned from query"); result = "Could not locate tuna" } }).catch((err) => { console.error('Failed to query successfully :: ' + err); result = 'Failed to query successfully :: ' + err }); // await ctx.render('index', { // title, result // }) return result } exports.excuteQuery = excuteQuery exports.executeTranSaction = executeTranSaction exports.excuteRecall = excuteRecall
修改调用层
const tool = require('./tool') module.exports = { //插入轨迹 async insertTrail (ctx) { const queryBody = ctx.request.query; const Trid = queryBody.Trid; console.log(`Trid.....${Trid}`) const jsonStr = queryBody.jsonStr; console.log(`jsonStr.....${jsonStr}`) var tx_id = null; const request = { //targets : --- letting this default to the peers assigned to the channel chaincodeId: 'trail', fcn: 'insertTrail', args : [Trid,jsonStr], chainId: 'mychannel', txId: tx_id }; ctx.body = await tool.executeTranSaction(request); }, //查询所有轨迹 async searchAllTrail ( ctx ) { var tx_id = null; const request = { chaincodeId: 'trail', txId: tx_id, fcn: 'searchAllTrail', args: [""], }; ctx.body = await tool.excuteQuery(request); }, async searchOneTrail ( ctx ) { const queryBody = ctx.request.query; const trid = queryBody.trid; console.log(`trid.....${trid}`) var tx_id = null; const request = { chaincodeId: 'trail', txId: tx_id, fcn: 'searchOneTrail', args: [trid], }; ctx.body = await tool.excuteQuery(request); }, //删除轨迹(世界状态) async deleteTrail (ctx) { const queryBody = ctx.request.query; const Trid = queryBody.Trid; console.log(`Trid.....${Trid}`) var tx_id = null; const request = { //targets : --- letting this default to the peers assigned to the channel chaincodeId: 'trail', fcn: 'deleteTrail', args : [Trid], chainId: 'mychannel', txId: tx_id }; ctx.body = await this.executeTranSaction(request); }, //更新轨迹(世界状态) async updateTrail (ctx) { const queryBody = ctx.request.query; const Trid = queryBody.Trid; console.log(`Trid.....${Trid}`) const jsonStr = queryBody.jsonStr; console.log(`jsonStr.....${jsonStr}`) var tx_id = null; const request = { //targets : --- letting this default to the peers assigned to the channel chaincodeId: 'trail', fcn: 'updateTrail', args : [Trid,jsonStr], chainId: 'mychannel', txId: tx_id }; ctx.body = await tool.executeTranSaction(request) }, // 回溯交易 async searchBlockByHash ( ctx ) { const queryBody = ctx.request.query; const hash = queryBody.hash; console.log(`hash.....${hash}`) ctx.body = await tool.excuteRecall(hash) }, async searchTrailByid ( ctx ) { const queryBody = ctx.request.query; const userid = queryBody.userid; var tx_id = null; console.log(`userid.....${userid}`) console.log(userid); const request = { chaincodeId: 'trail', txId: tx_id, fcn: 'searchTrailByid', args: [userid], }; ctx.body = await tool.excuteQuery(request); }, // 获取对应id所有数据的存放记录(可以查询到原始数据) async getHistoryForTrail ( ctx ) { const queryBody = ctx.request.query; const trid = queryBody.trid; console.log(`trid.....${trid}`) console.log(trid); var tx_id = null; const request = { chaincodeId: 'trail', txId: tx_id, fcn: 'getHistoryForTrail', args: [trid], }; ctx.body = await tool.excuteQuery(request) }, // 查询所有在世界状态上删除的轨迹数据 async getDeletedTrails ( ctx ) { var tx_id = null; const request = { chaincodeId: 'trail', txId: tx_id, fcn: 'getDeletedTrails', args: [""], }; ctx.body = await tool.excuteQuery(request) }, }
以便后续进行进一步的业务逻辑集成
二、链码整理
package main import( "fmt" "encoding/json" "bytes" "github.com/hyperledger/fabric/core/chaincode/shim" "github.com/hyperledger/fabric/protos/peer" ) type TrailChaincode struct { } type point struct{ Locationx float64 `json:"locationx"` Locationy float64 `json:"locationy"` // Speed float64 `json:"speed"` // Locatetime int `json:"locatetime"` // Direction float64 `json:"direction"` // Height float64 `json:"height"` } type Trail struct { Username string `json:"username"` Userid string `json:"userid"` Devid string `json:"devid"` Devstr string`json:"devstr"` Trid string `json:"trid"` Points []point `json:"points"` Mile float64 `json:"mile"` State bool `json:"state"` Hash string `json:"hash"` } type DeletedTrails struct { Num int `json:"num"` Trids []string `json:"trids"` } func (t *TrailChaincode) Init(stub shim.ChaincodeStubInterface) peer.Response { //------------------------------------------------------------- var deletedTrails DeletedTrails deletedTrailsAsBytes, _ := json.Marshal(deletedTrails) err := stub.PutState("deletedTrails", deletedTrailsAsBytes) if err != nil { shim.Error(err.Error()) } //------------------------------------------------------------- return shim.Success(nil) } func (t *TrailChaincode) Invoke(stub shim.ChaincodeStubInterface) peer.Response { fn , args := stub.GetFunctionAndParameters() if fn == "insertTrail" { return t.insertTrail(stub,args) } else if fn == "searchAllTrail" { return t.searchAllTrail(stub,args) } else if fn == "searchOneTrail" { return t.searchOneTrail(stub,args) } else if fn == "deleteTrail" { return t.deleteTrail(stub,args) } else if fn == "updateTrail" { return t.updateTrail(stub,args) } else if fn == "searchTrailByid" { return t.searchTrailByid(stub,args) } else if fn == "getHistoryForTrail" { return t.getHistoryForTrail(stub,args) } else if fn == "getDeletedTrails" { return t.getDeletedTrails(stub,args) } return shim.Error("Invoke 调用方法有误!") } func (t *TrailChaincode) insertTrail(stub shim.ChaincodeStubInterface , args []string) peer.Response{ //序列化 // trail := Trail{} // jsonStr := args[0] // err := json.Unmarshal([]byte(jsonStr), &trail) // if err != nil { // return shim.Error(err.Error()) // } // accBytes, err := json.Marshal(trail) // if err != nil { // return shim.Error(err.Error()) // } //保存数据 // err = stub.PutState(trail.trid, accBytes) Trid := args[0] jsonStr := args[1] err := stub.PutState(Trid, []byte(jsonStr)) if err != nil { shim.Error(err.Error()) } return shim.Success([]byte("insert 写入账本成功!")) } func (t *TrailChaincode) updateTrail(stub shim.ChaincodeStubInterface , args []string) peer.Response{ Trid := args[0] jsonStr := args[1] err := stub.PutState(Trid, []byte(jsonStr)) if err != nil { shim.Error(err.Error()) } return shim.Success([]byte("insert 更新账本成功!")) } //查询所有的轨迹 func (t *TrailChaincode) deleteTrail(stub shim.ChaincodeStubInterface , args []string) peer.Response{ Trid := args[0] err := stub.DelState(Trid) if err != nil { shim.Error(err.Error()) } //----------------------------------------------------------------- //记录删除的轨迹编号 result, err2 := stub.GetState("deletedTrails") if err2 != nil { return shim.Error("查询轨迹失败!") } //查询已删除的轨迹编号类 var deletedTrails DeletedTrails err3 := json.Unmarshal([]byte(result),&deletedTrails) if err3 != nil { return shim.Error(err3.Error()) } deletedTrails.Num += 1; deletedTrails.Trids = append(deletedTrails.Trids, Trid) //删除数+1,append删除的轨迹编号 deletedTrailsAsBytes, _ := json.Marshal(deletedTrails) err4 := stub.PutState("deletedTrails", deletedTrailsAsBytes) if err4 != nil { shim.Error(err4.Error()) } //重新转bytes放入 //----------------------------------------------------------------- return shim.Success([]byte("delete 删除账本成功!")) } //删除指定轨迹 func (t *TrailChaincode) searchAllTrail(stub shim.ChaincodeStubInterface, args []string) peer.Response{ fmt.Println("start searchAllTrail") // 获取所有用户的票数 resultIterator, err := stub.GetStateByRange("","") if err != nil { return shim.Error("查询所有的轨迹失败!") } defer resultIterator.Close() var buffer bytes.Buffer buffer.WriteString("[") isWritten := false for resultIterator.HasNext() { queryResult , err := resultIterator.Next() if err != nil { return shim.Error(err.Error()) } if isWritten == true { buffer.WriteString(",") } buffer.WriteString(string(queryResult.Value)) isWritten = true } buffer.WriteString("]") fmt.Printf("查询结果:\n%s\n",buffer.String()) fmt.Println("end searchAllTrail") return shim.Success(buffer.Bytes()) } //查询所有的轨迹 func (t *TrailChaincode) searchOneTrail(stub shim.ChaincodeStubInterface, args []string) peer.Response{ fmt.Println("start searchOneTrail") trid := args[0] result, err := stub.GetState(trid) if err != nil { return shim.Error("查询所有的轨迹失败!") } return shim.Success(result) } func SliceRemove(s []Trail, index int) []Trail { return append(s[:index], s[index+1:]...) } //通过电话号查所有轨迹 func (t *TrailChaincode) searchTrailByid(stub shim.ChaincodeStubInterface, args []string) peer.Response{ idpar := args[0] var revMsg Trail fmt.Println("start searchTrailByid") var trails []Trail; // 获取所有用户的票数 resultIterator, err := stub.GetStateByRange("","") if err != nil { return shim.Error("查询所有的轨迹失败!") } defer resultIterator.Close() // var buffer bytes.Buffer // buffer.WriteString("[") // isWritten := false for resultIterator.HasNext() { queryResult , err := resultIterator.Next() if err != nil { return shim.Error(err.Error()) } // if isWritten == true { // buffer.WriteString(",") // } err1 := json.Unmarshal([]byte(queryResult.Value),&revMsg) // shim.Error(revMsg) if err1 != nil { return shim.Error(err.Error()) } if revMsg.Userid == idpar { // str, err2 := json.Marshal(revMsg) // if err2 != nil { // return shim.Error(err.Error()) // } // buffer.WriteString(string(str)) trails = append(trails, revMsg) // isWritten = true } // else { isWritten = false } } // buffer.WriteString("]") if len(trails) != 0 { trails = SliceRemove(trails,len(trails)-1) } fmt.Printf("- searchTrailByid returning:\n%s", trails) //change to array of bytes trailsAsBytes, _ := json.Marshal(trails) //convert to array of bytes return shim.Success(trailsAsBytes) } func (t *TrailChaincode) getHistoryForTrail(stub shim.ChaincodeStubInterface, args []string) peer.Response{ type AuditHistory struct { TxId string `json:"txId"` Value Trail `json:"value"` } var history []AuditHistory var trail Trail if len(args) != 1 { return shim.Error("参数个数错误,应为1!") } trid := args[0] fmt.Printf("- 开始历史回溯: %s\n", trid) // Get History resultsIterator, err := stub.GetHistoryForKey(trid) if err != nil { return shim.Error(err.Error()) } defer resultsIterator.Close() for resultsIterator.HasNext() { historyData, err := resultsIterator.Next() if err != nil { return shim.Error(err.Error()) } var tx AuditHistory tx.TxId = historyData.TxId //copy transaction id over json.Unmarshal(historyData.Value, &trail) //un stringify it aka JSON.parse() if historyData.Value == nil { //trail has been deleted var emptyTrail Trail tx.Value = emptyTrail //copy nil trail } else { json.Unmarshal(historyData.Value, &trail) //un stringify it aka JSON.parse() tx.Value = trail //copy trail over } history = append(history, tx) //add this tx to the list } fmt.Printf("- getHistoryForTrail returning:\n%s", history) //change to array of bytes historyAsBytes, _ := json.Marshal(history) //convert to array of bytes return shim.Success(historyAsBytes) } // 查询删除过的轨迹信息 func (t *TrailChaincode) getDeletedTrails(stub shim.ChaincodeStubInterface, args []string) peer.Response{ type AuditHistory struct { TxId string `json:"txId"` Value Trail `json:"value"` } var finalhistory []AuditHistory var history []AuditHistory var trail Trail // Get History result, _ := stub.GetState("deletedTrails") //查询已删除的轨迹编号类 var deletedTrails DeletedTrails json.Unmarshal([]byte(result),&deletedTrails) //[]byte转结构体 for _, trid := range deletedTrails.Trids { resultsIterator, err := stub.GetHistoryForKey(trid) if err != nil { return shim.Error(err.Error()) } defer resultsIterator.Close() for resultsIterator.HasNext() { historyData, err := resultsIterator.Next() if err != nil { return shim.Error(err.Error()) } var tx AuditHistory tx.TxId = historyData.TxId //copy transaction id over json.Unmarshal(historyData.Value, &trail) //un stringify it aka JSON.parse() if historyData.Value == nil { //trail has been deleted var emptyTrail Trail tx.Value = emptyTrail //copy nil trail } else { json.Unmarshal(historyData.Value, &trail) //un stringify it aka JSON.parse() tx.Value = trail //copy trail over } history = append(history, tx) //add this tx to the list } if history[len(history)-1].Value.Userid == "" { finalhistory = append(finalhistory, history[len(history)-2]) } history = []AuditHistory{} } fmt.Printf("- getfinalhistory returning:\n%s", finalhistory) //change to array of bytes finalhistoryAsBytes, _ := json.Marshal(finalhistory) //convert to array of bytes return shim.Success(finalhistoryAsBytes) } func main(){ err := shim.Start(new(TrailChaincode)) if err != nil { fmt.Println("Trail chaincode start err") } }
这篇关于基于hyperledger联盟链的汽车轨迹追溯系统(一)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南